package top.gokzzz.log.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import top.gokzzz.common.utils.BeanUtil;
import top.gokzzz.common.utils.ServletUtil;
import top.gokzzz.common.utils.ThrowableUtil;
import top.gokzzz.log.annotation.ApiLog;
import top.gokzzz.log.dao.SysApiLogDao;
import top.gokzzz.log.domain.SysApiLog;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 系統 API 日誌
 *
 * @author Gok
 * @version 1.0
 */
@Component
@Aspect
public class ApiLogAspect {
    private final SysApiLogDao sysApiLogDao;

    ThreadLocal<Long> currentTime = new ThreadLocal<>();

    public ApiLogAspect(SysApiLogDao sysApiLogDao) {
        this.sysApiLogDao = sysApiLogDao;
    }

    /**
     * 切入點
     */
    @Pointcut("@annotation(top.gokzzz.log.annotation.ApiLog)")
    public void logPointcut() {
    }

    /**
     * 環繞通知
     */
    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result;
        currentTime.set(System.currentTimeMillis());
        result = joinPoint.proceed();

        saveLog(buildSysApiLog(joinPoint, "INFO", System.currentTimeMillis() - currentTime.get(), null));

        return result;
    }

    /**
     * 異常通知
     */
    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        saveLog(buildSysApiLog((ProceedingJoinPoint) joinPoint, "ERROR", System.currentTimeMillis() - currentTime.get(), e));
        currentTime.remove();
    }

    /**
     * 保存日誌
     *
     * @param sysApiLog 系統 API 日誌
     */
    @Async
    public void saveLog(SysApiLog sysApiLog) {
        sysApiLog.setId(UUID.randomUUID().toString());
        sysApiLogDao.save(sysApiLog);
    }

    /**
     * 構建 SysApiLog
     *
     * @param joinPoint 端點
     * @param logType   日誌類型
     * @param time      耗時
     * @param e         異常
     * @return {@link SysApiLog }
     */
    private SysApiLog buildSysApiLog(ProceedingJoinPoint joinPoint, String logType, Long time, Throwable e) {
        SysApiLog sysApiLog = new SysApiLog();
        sysApiLog.setLogType(logType);
        sysApiLog.setTime(time);

        HttpServletRequest request = ServletUtil.getRequest();
        sysApiLog.setRequestIp(ServletUtil.getIp());
        sysApiLog.setUrl(request.getRequestURL().toString());

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        sysApiLog.setMethod(joinPoint.getTarget().getClass().getName() + "." + method.getName() + "()");
        sysApiLog.setDescription(method.getAnnotation(ApiLog.class).value());

        sysApiLog.setParams(extractParams(joinPoint, method));

        if (e != null) {
            sysApiLog.setExceptionDetail(ThrowableUtil.getStackTrace(e).getBytes());
        }
        return sysApiLog;
    }

    /**
     * 提取參數
     *
     * @param joinPoint 端點
     * @param method    方法
     * @return {@link String }
     */
    private String extractParams(ProceedingJoinPoint joinPoint, Method method) {
        Parameter[] parameters = method.getParameters();
        Map<String, Object> paramMap = new LinkedHashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            Object paramValue = joinPoint.getArgs()[i];
            if (isParamAllowed(paramValue)) {
                paramMap.put(parameters[i].getName(), paramValue);
            }
        }
        return BeanUtil.bean2Json(paramMap);
    }

    /**
     * 是否允許
     *
     * @param paramValue 參數
     * @return boolean
     */
    private boolean isParamAllowed(Object paramValue) {
        return !(paramValue instanceof MultipartFile ||
                paramValue instanceof HttpServletResponse ||
                paramValue instanceof HttpServletRequest ||
                (paramValue instanceof List && ((List<?>) paramValue).stream().allMatch(o -> o instanceof MultipartFile)));
    }

}
